import {
  convertOtelSpanToIngestionEvent,
  convertNanoTimestampToISO,
} from "@/src/features/otel/server";
import { ingestionEvent } from "@langfuse/shared/src/server";

describe("OTel Resource Span Mapping", () => {
  describe("Langfuse OTEL SDK spans", () => {
    const publicKey = "pk-lf-1234567890";

    it("should convert LF-OTEL spans to LF-events", () => {
      const langfuseOtelSpans = [
        {
          resource: {
            attributes: [
              {
                key: "telemetry.sdk.language",
                value: { stringValue: "python" },
              },
              {
                key: "telemetry.sdk.name",
                value: { stringValue: "opentelemetry" },
              },
              {
                key: "telemetry.sdk.version",
                value: { stringValue: "1.32.0" },
              },
              {
                key: "langfuse.environment",
                value: { stringValue: "production" },
              },
              { key: "langfuse.release", value: { stringValue: "0.0.1" } },
              {
                key: "service.name",
                value: { stringValue: "unknown_service" },
              },
            ],
          },
          scopeSpans: [
            {
              scope: {
                name: "langfuse-sdk",
                version: "2.60.3",
                attributes: [
                  {
                    key: "public_key",
                    value: { stringValue: "pk-lf-1234567890" },
                  },
                ],
              },
              spans: [
                {
                  traceId: {
                    type: "Buffer",
                    data: [
                      44, 206, 24, 247, 232, 205, 6, 90, 11, 78, 99, 78, 239,
                      114, 131, 145,
                    ],
                  },
                  spanId: {
                    type: "Buffer",
                    data: [87, 240, 37, 84, 23, 151, 65, 189],
                  },
                  parentSpanId: {
                    type: "Buffer",
                    data: [223, 227, 135, 254, 167, 239, 59, 2],
                  },
                  name: "my-generation",
                  kind: 1,
                  startTimeUnixNano: {
                    low: 466848096,
                    high: 406528574,
                    unsigned: true,
                  },
                  endTimeUnixNano: {
                    low: 467248096,
                    high: 406528574,
                    unsigned: true,
                  },
                  attributes: [
                    {
                      key: "langfuse.observation.type",
                      value: { stringValue: "generation" },
                    },
                    {
                      key: "langfuse.observation.level",
                      value: { stringValue: "WARNING" },
                    },
                    {
                      key: "langfuse.observation.status_message",
                      value: { stringValue: "nothing to report" },
                    },
                    {
                      key: "langfuse.observation.input",
                      value: {
                        stringValue: '[{"role": "user", "content": "hello"}]',
                      },
                    },
                    {
                      key: "langfuse.observation.output",
                      value: {
                        stringValue:
                          '{"role": "assistant", "content": "what\'s up?"}',
                      },
                    },
                    {
                      key: "langfuse.observation.model.name",
                      value: { stringValue: "gpt-4o" },
                    },
                    {
                      key: "langfuse.observation.prompt.name",
                      value: { stringValue: "my-prompt" },
                    },
                    {
                      key: "langfuse.observation.prompt.version",
                      value: { intValue: { low: 1, high: 0, unsigned: false } },
                    },
                    {
                      key: "langfuse.observation.usage_details",
                      value: {
                        stringValue:
                          '{"input_tokens": 123, "output_tokens": 456}',
                      },
                    },
                    {
                      key: "langfuse.observation.cost_details",
                      value: {
                        stringValue:
                          '{"input_tokens": 0.0001, "output_tokens": 0.002}',
                      },
                    },
                    {
                      key: "langfuse.observation.completion_start_time",
                      value: { stringValue: '"2025-04-30T15:28:50.686390Z"' },
                    },
                    {
                      key: "langfuse.observation.model_parameters",
                      value: {
                        stringValue: '{"temperature": 1, "top_p": "0.2"}',
                      },
                    },
                    {
                      key: "langfuse.observation.metadata.key1",
                      value: { stringValue: '"value1"' },
                    },
                    {
                      key: "langfuse.observation.metadata.key2",
                      value: { stringValue: '"value2"' },
                    },
                    {
                      key: "langfuse.trace.name",
                      value: { stringValue: "test-trace" },
                    },
                    { key: "user.id", value: { stringValue: "my-user" } },
                    { key: "session.id", value: { stringValue: "my-session" } },
                    {
                      key: "langfuse.version",
                      value: { stringValue: "trace-0.0.1" },
                    },
                    {
                      key: "langfuse.trace.input",
                      value: {
                        stringValue: '[{"role": "user", "content": "hello"}]',
                      },
                    },
                    {
                      key: "langfuse.trace.output",
                      value: {
                        stringValue:
                          '{"role": "assistant", "content": "what\'s up?"}',
                      },
                    },
                    {
                      key: "langfuse.trace.tags",
                      value: {
                        arrayValue: { values: [{ stringValue: "tag2" }] },
                      },
                    },
                    {
                      key: "langfuse.trace.public",
                      value: { boolValue: true },
                    },
                    {
                      key: "langfuse.trace.metadata.trace-key1",
                      value: { stringValue: '"value1"' },
                    },
                    {
                      key: "langfuse.trace.metadata.trace-key2",
                      value: { stringValue: '"value2"' },
                    },
                  ],
                  status: {},
                },
                {
                  traceId: {
                    type: "Buffer",
                    data: [
                      44, 206, 24, 247, 232, 205, 6, 90, 11, 78, 99, 78, 239,
                      114, 131, 145,
                    ],
                  },
                  spanId: {
                    type: "Buffer",
                    data: [223, 227, 135, 254, 167, 239, 59, 2],
                  },
                  name: "my-span",
                  kind: 1,
                  startTimeUnixNano: {
                    low: 205413096,
                    high: 406528574,
                    unsigned: true,
                  },
                  endTimeUnixNano: {
                    low: 467608096,
                    high: 406528574,
                    unsigned: true,
                  },
                  attributes: [
                    {
                      key: "langfuse.observation.type",
                      value: { stringValue: "span" },
                    },
                    {
                      key: "langfuse.trace.tags",
                      value: {
                        arrayValue: { values: [{ stringValue: "tag1" }] },
                      },
                    },
                    {
                      key: "langfuse.trace.metadata.trace-key0",
                      value: { stringValue: '"value1"' },
                    },
                  ],
                  status: {},
                },
              ],
            },
          ],
        },
      ];

      const events = langfuseOtelSpans.flatMap((span) =>
        convertOtelSpanToIngestionEvent(span, publicKey),
      );
      const traceEvents = events.filter((e) => e.type === "trace-create");
      const generationEvents = events.filter(
        (e) => e.type === "generation-create",
      );
      const spanEvents = events.filter((e) => e.type === "span-create");

      expect(events.length).toBe(4);
      expect(traceEvents.length).toBe(2);
      expect(spanEvents.length).toBe(1);
      expect(generationEvents.length).toBe(1);

      const spanEvent = spanEvents[0];
      const generationEvent = generationEvents[0];

      const defaultMetadata = {
        resourceAttributes: {
          "langfuse.environment": "production",
          "langfuse.release": "0.0.1",
          "service.name": "unknown_service",
          "telemetry.sdk.language": "python",
          "telemetry.sdk.name": "opentelemetry",
          "telemetry.sdk.version": "1.32.0",
        },
        scope: {
          name: "langfuse-sdk",
          version: "2.60.3",
        },
      };

      expect(spanEvent.body).toMatchObject({
        id: "dfe387fea7ef3b02",
        traceId: "2cce18f7e8cd065a0b4e634eef728391",
        parentObservationId: null,
        name: "my-span",
        startTime: "2025-04-30T15:28:50.424Z",
        endTime: "2025-04-30T15:28:50.687Z",
        environment: "production",
        metadata: { ...defaultMetadata },
        level: "DEFAULT",
        version: null,
        modelParameters: {},
        model: undefined,
        promptName: null,
        promptVersion: null,
        usageDetails: {},
        costDetails: {},
        input: null,
        output: null,
      });

      expect(generationEvent.body).toMatchObject({
        id: "57f02554179741bd",
        traceId: "2cce18f7e8cd065a0b4e634eef728391",
        parentObservationId: "dfe387fea7ef3b02",
        name: "my-generation",
        startTime: "2025-04-30T15:28:50.686Z",
        endTime: "2025-04-30T15:28:50.686Z",
        environment: "production",
        metadata: { ...defaultMetadata },
        level: "WARNING",
        statusMessage: "nothing to report",
        version: "trace-0.0.1",
        modelParameters: {},
        model: "gpt-4o",
        promptName: "my-prompt",
        promptVersion: 1,
        usageDetails: { input_tokens: 123, output_tokens: 456 },
        costDetails: { input_tokens: 0.0001, output_tokens: 0.002 },
        input: '[{"role": "user", "content": "hello"}]',
        output: '{"role": "assistant", "content": "what\'s up?"}',
      });

      expect(traceEvents[0].body).toMatchObject({
        id: "2cce18f7e8cd065a0b4e634eef728391",
        name: "test-trace",
        tags: ["tag2"],
        userId: "my-user",
        sessionId: "my-session",
        input: '[{"role": "user", "content": "hello"}]',
        output: '{"role": "assistant", "content": "what\'s up?"}',
        version: "trace-0.0.1",
        environment: "production",
        public: true,
        metadata: {
          ...defaultMetadata,
          "trace-key1": '"value1"',
          "trace-key2": '"value2"',
        },
      });

      expect(traceEvents[1].body).toMatchObject({
        id: "2cce18f7e8cd065a0b4e634eef728391",
        timestamp: "2025-04-30T15:28:50.424Z",
        name: "my-span",
        metadata: {
          ...defaultMetadata,
          "trace-key0": '"value1"',
        },
        tags: ["tag1"],
        environment: "production",
      });
    });

    it("should create a trace when as_root has been specified", () => {
      const langfuseOtelSpans = [
        {
          resource: {
            attributes: [
              {
                key: "telemetry.sdk.language",
                value: { stringValue: "python" },
              },
              {
                key: "telemetry.sdk.name",
                value: { stringValue: "opentelemetry" },
              },
              {
                key: "telemetry.sdk.version",
                value: { stringValue: "1.32.0" },
              },
              {
                key: "langfuse.environment",
                value: { stringValue: "production" },
              },
              { key: "langfuse.release", value: { stringValue: "0.0.1" } },
              {
                key: "service.name",
                value: { stringValue: "unknown_service" },
              },
            ],
          },
          scopeSpans: [
            {
              scope: {
                name: "langfuse-sdk",
                version: "2.60.3",
                attributes: [
                  {
                    key: "public_key",
                    value: { stringValue: "pk-lf-1234567890" },
                  },
                ],
              },
              spans: [
                {
                  traceId: {
                    type: "Buffer",
                    data: [
                      149, 243, 185, 38, 199, 208, 9, 146, 91, 203, 93, 188, 39,
                      49, 17, 32,
                    ],
                  },
                  spanId: {
                    type: "Buffer",
                    data: [212, 62, 55, 183, 209, 126, 84, 118],
                  },
                  parentSpanId: {
                    type: "Buffer",
                    data: [131, 78, 40, 181, 145, 127, 190, 246],
                  },
                  name: "my-span-with-custom-trace-id",
                  kind: 1,
                  startTimeUnixNano: {
                    low: 1047784088,
                    high: 406627672,
                    unsigned: true,
                  },
                  endTimeUnixNano: {
                    low: 1149405088,
                    high: 406627672,
                    unsigned: true,
                  },
                  attributes: [
                    {
                      key: "langfuse.observation.type",
                      value: { stringValue: "span" },
                    },
                    {
                      key: "langfuse.internal.as_root",
                      value: { boolValue: true },
                    },
                  ],
                  status: {},
                },
              ],
            },
          ],
        },
      ];

      const events = langfuseOtelSpans.flatMap((span) =>
        convertOtelSpanToIngestionEvent(span, publicKey),
      );
      const traceEvents = events.filter((e) => e.type === "trace-create");
      const spanEvents = events.filter((e) => e.type === "span-create");

      expect(events.length).toBe(2);
      expect(traceEvents.length).toBe(1);
      expect(spanEvents.length).toBe(1);

      const spanEvent = spanEvents[0];
      const traceEvent = traceEvents[0];

      expect(spanEvent.body).toMatchObject({
        id: "d43e37b7d17e5476",
        traceId: "95f3b926c7d009925bcb5dbc27311120",
        parentObservationId: "834e28b5917fbef6",
        name: "my-span-with-custom-trace-id",
        startTime: "2025-05-05T13:42:33.936Z",
        endTime: "2025-05-05T13:42:34.038Z",
        environment: "production",
        level: "DEFAULT",
      });

      expect(traceEvent.body).toMatchObject({
        id: "95f3b926c7d009925bcb5dbc27311120",
        timestamp: "2025-05-05T13:42:33.936Z",
        name: undefined,
        environment: "production",
      });
    });
    it("should throw an error if langfuse scope spans have wrong project ID", () => {
      const langfuseOtelSpans = [
        {
          resource: {
            attributes: [
              {
                key: "telemetry.sdk.language",
                value: {
                  stringValue: "python",
                },
              },
              {
                key: "telemetry.sdk.name",
                value: {
                  stringValue: "opentelemetry",
                },
              },
              {
                key: "telemetry.sdk.version",
                value: {
                  stringValue: "1.32.0",
                },
              },
              {
                key: "service.name",
                value: {
                  stringValue: "unknown_service",
                },
              },
            ],
          },
          scopeSpans: [
            {
              scope: {
                name: "langfuse-sdk",
                version: "2.60.3",
                attributes: [
                  {
                    key: "public_key",
                    value: {
                      stringValue: "pk-lf-another",
                    },
                  },
                ],
              },
              spans: [
                {
                  traceId: {
                    type: "Buffer",
                    data: [
                      219, 242, 249, 255, 154, 168, 21, 165, 233, 52, 222, 186,
                      28, 97, 54, 95,
                    ],
                  },
                  spanId: {
                    type: "Buffer",
                    data: [128, 191, 93, 22, 69, 180, 81, 135],
                  },
                  name: "t1",
                  kind: 1,
                  startTimeUnixNano: {
                    low: -1422874264,
                    high: 406650983,
                    unsigned: true,
                  },
                  endTimeUnixNano: {
                    low: -904695264,
                    high: 406650983,
                    unsigned: true,
                  },
                  attributes: [
                    {
                      key: "langfuse.observation.type",
                      value: {
                        stringValue: "span",
                      },
                    },
                  ],
                  status: {},
                },
              ],
            },
          ],
        },
      ];

      expect(() =>
        langfuseOtelSpans.flatMap((span) =>
          convertOtelSpanToIngestionEvent(span, publicKey),
        ),
      ).toThrowError("Langfuse OTEL SDK span has different public key");
    });
  });

  describe("Vendor Spans", () => {
    it("should convert an OpenLit OTel Span to Langfuse Events", () => {
      // Setup
      const resourceSpan = {
        resource: {
          attributes: [
            {
              key: "telemetry.sdk.language",
              value: { stringValue: "python" },
            },
            { key: "telemetry.sdk.name", value: { stringValue: "openlit" } },
            {
              key: "telemetry.sdk.version",
              value: { stringValue: "1.27.0" },
            },
            { key: "service.name", value: { stringValue: "default" } },
            {
              key: "deployment.environment",
              value: { stringValue: "default" },
            },
          ],
        },
        scopeSpans: [
          {
            scope: { name: "openlit.otel.tracing" },
            spans: [
              {
                traceId: {
                  type: "Buffer",
                  data: [
                    234, 103, 55, 8, 68, 28, 41, 132, 165, 74, 62, 57, 98, 211,
                    89, 95,
                  ],
                },
                spanId: {
                  type: "Buffer",
                  data: [185, 4, 191, 251, 32, 190, 109, 126],
                },
                name: "openai.chat.completions",
                kind: 3,
                startTimeUnixNano: {
                  low: 153687506,
                  high: 404677085,
                  unsigned: true,
                },
                endTimeUnixNano: {
                  low: 1327836088,
                  high: 404677085,
                  unsigned: true,
                },
                attributes: [
                  {
                    key: "telemetry.sdk.name",
                    value: { stringValue: "openlit" },
                  },
                  { key: "gen_ai.system", value: { stringValue: "openai" } },
                  {
                    key: "gen_ai.operation.name",
                    value: { stringValue: "chat" },
                  },
                  {
                    key: "gen_ai.endpoint",
                    value: { stringValue: "openai.chat.completions" },
                  },
                  {
                    key: "gen_ai.response.id",
                    value: {
                      stringValue: "chatcmpl-AugxBIoQzz2zFMWFoiyS3Vmm1OuQI",
                    },
                  },
                  {
                    key: "gen_ai.environment",
                    value: { stringValue: "default" },
                  },
                  {
                    key: "gen_ai.application_name",
                    value: { stringValue: "default" },
                  },
                  {
                    key: "gen_ai.request.model",
                    value: { stringValue: "gpt-3.5-turbo" },
                  },
                  {
                    key: "gen_ai.request.top_p",
                    value: { doubleValue: 1 },
                  },
                  {
                    key: "gen_ai.request.max_tokens",
                    value: { intValue: { low: -1, high: -1, unsigned: false } },
                  },
                  {
                    key: "gen_ai.request.user",
                    value: { stringValue: "" },
                  },
                  {
                    key: "gen_ai.request.temperature",
                    value: { doubleValue: 1 },
                  },
                  {
                    key: "gen_ai.request.presence_penalty",
                    value: { doubleValue: 0 },
                  },
                  {
                    key: "gen_ai.request.frequency_penalty",
                    value: { doubleValue: 0 },
                  },
                  { key: "gen_ai.request.seed", value: { stringValue: "" } },
                  {
                    key: "gen_ai.request.is_stream",
                    value: { boolValue: false },
                  },
                  {
                    key: "gen_ai.usage.input_tokens",
                    value: { intValue: { low: 14, high: 0, unsigned: false } },
                  },
                  {
                    key: "gen_ai.usage.output_tokens",
                    value: { intValue: { low: 96, high: 0, unsigned: false } },
                  },
                  {
                    key: "gen_ai.usage.total_tokens",
                    value: { intValue: { low: 110, high: 0, unsigned: false } },
                  },
                  {
                    key: "gen_ai.response.finish_reasons",
                    value: {
                      arrayValue: { values: [{ stringValue: "stop" }] },
                    },
                  },
                  {
                    key: "gen_ai.usage.cost",
                    value: { doubleValue: 0.000151 },
                  },
                ],
                events: [
                  {
                    timeUnixNano: {
                      low: 1327691067,
                      high: 404677085,
                      unsigned: true,
                    },
                    name: "gen_ai.content.prompt",
                    attributes: [
                      {
                        key: "gen_ai.prompt",
                        value: {
                          stringValue: "user: What is LLM Observability?",
                        },
                      },
                    ],
                  },
                  {
                    timeUnixNano: {
                      low: 1327737136,
                      high: 404677085,
                      unsigned: true,
                    },
                    name: "gen_ai.content.completion",
                    attributes: [
                      {
                        key: "gen_ai.completion",
                        value: {
                          stringValue:
                            "LLM Observability stands for logs, metrics, and traces observability. It refers to the practice of monitoring and analyzing logs, metrics, and traces from a software application to gain insight into its performance, behavior, and issues. By collecting and analyzing these data points, developers and operators can better understand how the application is functioning and troubleshoot any problems that may arise. This approach is increasingly important in modern software development, where applications are often complex and distributed across multiple environments.",
                        },
                      },
                    ],
                  },
                ],
                status: { code: 1 },
              },
            ],
          },
        ],
      };

      // When
      const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

      // Then
      // Will throw an error if the parsing fails
      const parsedEvents = langfuseEvents.map((event) =>
        ingestionEvent.parse(event),
      );
      expect(parsedEvents).toHaveLength(2);
    });

    it("should convert a TraceLoop OTel Span to Langfuse Events", () => {
      // Setup
      const resourceSpan = {
        resource: {
          attributes: [
            {
              key: "service.name",
              value: {
                stringValue:
                  "/usr/local/lib/python3.11/dist-packages/colab_kernel_launcher.py",
              },
            },
          ],
        },
        scopeSpans: [
          {
            scope: {
              name: "opentelemetry.instrumentation.openai.v1",
              version: "0.33.9",
            },
            spans: [
              {
                traceId: {
                  type: "Buffer",
                  data: [
                    228, 239, 69, 2, 92, 155, 64, 146, 75, 255, 23, 94, 43, 18,
                    91, 91,
                  ],
                },
                spanId: {
                  type: "Buffer",
                  data: [170, 191, 22, 228, 22, 174, 73, 82],
                },
                name: "openai.chat",
                kind: 3,
                startTimeUnixNano: {
                  low: 865964564,
                  high: 404693214,
                  unsigned: true,
                },
                endTimeUnixNano: {
                  low: -1984808674,
                  high: 404693214,
                  unsigned: true,
                },
                attributes: [
                  { key: "llm.request.type", value: { stringValue: "chat" } },
                  { key: "gen_ai.system", value: { stringValue: "OpenAI" } },
                  {
                    key: "gen_ai.request.model",
                    value: { stringValue: "gpt-3.5-turbo" },
                  },
                  { key: "llm.headers", value: { stringValue: "None" } },
                  { key: "llm.is_streaming", value: { boolValue: false } },
                  {
                    key: "gen_ai.openai.api_base",
                    value: { stringValue: "https://api.openai.com/v1/" },
                  },
                  {
                    key: "gen_ai.prompt.0.role",
                    value: { stringValue: "user" },
                  },
                  {
                    key: "gen_ai.prompt.0.content",
                    value: { stringValue: "What is LLM Observability?" },
                  },
                  {
                    key: "gen_ai.response.model",
                    value: { stringValue: "gpt-3.5-turbo-0125" },
                  },
                  {
                    key: "llm.usage.total_tokens",
                    value: { intValue: { low: 187, high: 0, unsigned: false } },
                  },
                  {
                    key: "gen_ai.usage.completion_tokens",
                    value: { intValue: { low: 173, high: 0, unsigned: false } },
                  },
                  {
                    key: "gen_ai.usage.prompt_tokens",
                    value: { intValue: { low: 14, high: 0, unsigned: false } },
                  },
                  {
                    key: "gen_ai.completion.0.finish_reason",
                    value: { stringValue: "stop" },
                  },
                  {
                    key: "gen_ai.completion.0.role",
                    value: { stringValue: "assistant" },
                  },
                  {
                    key: "gen_ai.completion.0.content",
                    value: {
                      stringValue:
                        "LLM Observability (Logs, Metrics, and Traces Observability) is a strategy for monitoring and understanding the behavior of applications and systems in real-time. It involves collecting, analyzing, and correlating logs, metrics, and traces from various sources to gain insights into system performance, availability, and overall health.\n\nLogs provide detailed records of events and activities within a system, metrics offer quantitative measurements of system performance, and traces show the flow of requests through a system. By combining these sources of data, organizations can gain a comprehensive view of their systems and applications, identify issues quickly, and optimize performance.\n\nLLM Observability is essential for modern, complex distributed systems as it allows teams to troubleshoot issues, improve system performance, and enhance the overall user experience. It enables organizations to proactively monitor their systems and respond quickly to any issues that may arise.",
                    },
                  },
                ],
                status: {},
              },
            ],
          },
        ],
      };

      // When
      const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

      // Then
      // Will throw an error if the parsing fails
      const parsedEvents = langfuseEvents.map((event) =>
        ingestionEvent.parse(event),
      );
      expect(parsedEvents).toHaveLength(2);
    });

    it("LFE-5171: should convert a Semantic Kernel 1.55+ OTel Span with new event-based semantic conventions to Langfuse Events", () => {
      // Setup - Semantic Kernel 1.55+ uses new event names instead of deprecated gen_ai.content.prompt/completion
      const resourceSpan = {
        scopeSpans: [
          {
            scope: { name: "Microsoft.SemanticKernel" },
            spans: [
              {
                traceId: {
                  type: "Buffer",
                  data: [
                    234, 103, 55, 8, 68, 28, 41, 132, 165, 74, 62, 57, 98, 211,
                    89, 95,
                  ],
                },
                spanId: {
                  type: "Buffer",
                  data: [185, 4, 191, 251, 32, 190, 109, 126],
                },
                name: "chat_completion",
                kind: 3,
                startTimeUnixNano: {
                  low: 153687506,
                  high: 404677085,
                  unsigned: true,
                },
                endTimeUnixNano: {
                  low: 1327836088,
                  high: 404677085,
                  unsigned: true,
                },
                attributes: [
                  {
                    key: "gen_ai.response.model",
                    value: { stringValue: "gpt-4-0613" },
                  },
                ],
                events: [
                  {
                    timeUnixNano: {
                      low: 1327691067,
                      high: 404677085,
                      unsigned: true,
                    },
                    name: "gen_ai.system.message",
                    attributes: [
                      {
                        key: "content",
                        value: {
                          stringValue: "You are a helpful assistant.",
                        },
                      },
                    ],
                  },
                  {
                    timeUnixNano: {
                      low: 1327691068,
                      high: 404677085,
                      unsigned: true,
                    },
                    name: "gen_ai.user.message",
                    attributes: [
                      {
                        key: "content",
                        value: {
                          stringValue: "What is the capital of France?",
                        },
                      },
                    ],
                  },
                  {
                    timeUnixNano: {
                      low: 1327737136,
                      high: 404677085,
                      unsigned: true,
                    },
                    name: "gen_ai.choice",
                    attributes: [
                      {
                        key: "index",
                        value: {
                          intValue: { low: 0, high: 0, unsigned: false },
                        },
                      },
                      {
                        key: "finish_reason",
                        value: { stringValue: "stop" },
                      },
                      {
                        key: "message",
                        value: {
                          stringValue: JSON.stringify({
                            role: "assistant",
                            content: "The capital of France is Paris.",
                          }),
                        },
                      },
                    ],
                  },
                ],
                status: { code: 1 },
              },
            ],
          },
        ],
      };

      // When
      const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

      // Then
      // Will throw an error if the parsing fails
      const parsedEvents = langfuseEvents.map((event) =>
        ingestionEvent.parse(event),
      );
      expect(parsedEvents).toHaveLength(2);

      // Check that input contains both system and user messages
      const observationEvent = parsedEvents.find(
        (event) => event.type === "generation-create",
      );
      expect(observationEvent?.body.input).toEqual([
        {
          role: "system",
          content: "You are a helpful assistant.",
        },
        {
          role: "user",
          content: "What is the capital of France?",
        },
      ]);

      // Check that output contains the choice
      expect(observationEvent?.body.output).toEqual({
        index: 0,
        finish_reason: "stop",
        message: JSON.stringify({
          role: "assistant",
          content: "The capital of France is Paris.",
        }),
      });
    });
  });

  describe("Property Mapping", () => {
    const defaultSpanProps = {
      traceId: {
        type: "Buffer",
        data: [
          234, 103, 55, 8, 68, 28, 41, 132, 165, 74, 62, 57, 98, 211, 89, 95,
        ],
      },
      spanId: {
        type: "Buffer",
        data: [185, 4, 191, 251, 32, 190, 109, 126],
      },
      name: "openai.chat.completions",
      kind: 3,
      startTimeUnixNano: {
        low: 153687506,
        high: 404677085,
        unsigned: true,
      },
      endTimeUnixNano: {
        low: 1327836088,
        high: 404677085,
        unsigned: true,
      },
    };

    it("should interpret an empty buffer as an unset parentSpanId", async () => {
      // https://github.com/langchain4j/langchain4j/issues/2328#issuecomment-2686129552
      // Empty buffers where detected as truthy, i.e. behaved like they had a parent span.
      // Setup
      const resourceSpan = {
        scopeSpans: [
          {
            spans: [
              {
                ...defaultSpanProps,
                parentSpanId: {
                  type: "Buffer",
                  data: [],
                },
              },
            ],
          },
        ],
      };

      // When
      const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

      // Then
      // Expect a span and a trace to be created
      expect(langfuseEvents).toHaveLength(2);
    });

    it("should interpret openinference LLM calls as a generation", async () => {
      const resourceSpan = {
        scopeSpans: [
          {
            spans: [
              {
                ...defaultSpanProps,
                attributes: [
                  {
                    key: "openinference.span.kind",
                    value: { stringValue: "LLM" },
                  },
                ],
              },
            ],
          },
        ],
      };

      // When
      const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

      // Then
      // Check that we create a generation
      expect(
        langfuseEvents.some((event) => event.type === "generation-create"),
      ).toBe(true);
    });

    it("should use logfire.msg as span name", async () => {
      const resourceSpan = {
        scopeSpans: [
          {
            spans: [
              {
                ...defaultSpanProps,
                name: "wrong name",
                attributes: [
                  {
                    key: "logfire.msg",
                    value: { stringValue: "right name" },
                  },
                ],
              },
            ],
          },
        ],
      };

      // When
      const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

      // Then
      expect(langfuseEvents[0].body.name).toBe("right name");
      expect(langfuseEvents[1].body.name).toBe("right name");
    });

    it.each([
      [
        "should cast input_tokens from string to number",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.input_tokens",
          otelAttributeValue: { stringValue: "15" },
          entityAttributeKey: "usageDetails.input",
          entityAttributeValue: 15,
        },
      ],
      [
        "should extract environment on trace for langfuse.environment",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.environment",
          otelAttributeValue: { stringValue: "test" },
          entityAttributeKey: "environment",
          entityAttributeValue: "test",
        },
      ],
      [
        "should extract environment on observation for deployment.environment.name",
        {
          entity: "observation",
          otelAttributeKey: "deployment.environment.name",
          otelAttributeValue: { stringValue: "test" },
          entityAttributeKey: "environment",
          entityAttributeValue: "test",
        },
      ],
      [
        "should fallback to default on observation if no environment present",
        {
          entity: "observation",
          otelAttributeKey: "unused.key",
          otelAttributeValue: { stringValue: "" },
          entityAttributeKey: "environment",
          entityAttributeValue: "default",
        },
      ],
      [
        "should extract promptName on observation from langfuse.prompt.name",
        {
          entity: "observation",
          otelAttributeKey: "langfuse.prompt.name",
          otelAttributeValue: { stringValue: "test" },
          entityAttributeKey: "promptName",
          entityAttributeValue: "test",
        },
      ],
      [
        "should extract public on trace from langfuse.public",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.public",
          otelAttributeValue: { boolValue: true },
          entityAttributeKey: "public",
          entityAttributeValue: true,
        },
      ],
      [
        "should not treat truthy values as public true",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.public",
          otelAttributeValue: { stringValue: "false" },
          entityAttributeKey: "public",
          entityAttributeValue: false,
        },
      ],
      [
        "should extract userId on trace from user.id",
        {
          entity: "trace",
          otelAttributeKey: "user.id",
          otelAttributeValue: { stringValue: "user-1" },
          entityAttributeKey: "userId",
          entityAttributeValue: "user-1",
        },
      ],
      [
        "should extract userId on trace from langfuse.user.id",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.user.id",
          otelAttributeValue: { stringValue: "user-1" },
          entityAttributeKey: "userId",
          entityAttributeValue: "user-1",
        },
      ],
      [
        "should extract sessionId on trace from session.id",
        {
          entity: "trace",
          otelAttributeKey: "session.id",
          otelAttributeValue: { stringValue: "session-1" },
          entityAttributeKey: "sessionId",
          entityAttributeValue: "session-1",
        },
      ],
      [
        "should extract sessionId on trace from langfuse.session.id",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.session.id",
          otelAttributeValue: { stringValue: "session-1" },
          entityAttributeKey: "sessionId",
          entityAttributeValue: "session-1",
        },
      ],
      [
        "should extract providedModelName from gen_ai.request.model",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.request.model",
          otelAttributeValue: { stringValue: "gpt-4" },
          entityAttributeKey: "model",
          entityAttributeValue: "gpt-4",
        },
      ],
      [
        "should extract providedModelName from llm.model_name",
        {
          entity: "observation",
          otelAttributeKey: "llm.model_name",
          otelAttributeValue: { stringValue: "gpt-4" },
          entityAttributeKey: "model",
          entityAttributeValue: "gpt-4",
        },
      ],
      [
        "should extract modelParameters from gen_ai.request (request.temperature)",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.request.temperature",
          otelAttributeValue: { doubleValue: 1 },
          entityAttributeKey: "modelParameters.temperature",
          entityAttributeValue: 1,
        },
      ],
      [
        "should extract modelParameters from gen_ai.request (request.max_tokens)",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.request.max_tokens",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "modelParameters.max_tokens",
          entityAttributeValue: 100,
        },
      ],
      [
        "should extract usage from llm.token_count (input_tokens)",
        {
          entity: "observation",
          otelAttributeKey: "llm.token_count.prompt",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.input",
          entityAttributeValue: 100,
        },
      ],
      [
        "should extract usage from gen_ai.usage (input_tokens)",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.input_tokens",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.input",
          entityAttributeValue: 100,
        },
      ],
      [
        "should extract usage from gen_ai.usage (completion_tokens)",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.completion_tokens",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.output",
          entityAttributeValue: 100,
        },
      ],
      [
        "should extract usage from gen_ai.usage (total_tokens)",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.total_tokens",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.total",
          entityAttributeValue: 100,
        },
      ],
      [
        "should extract usage from gen_ai.usage (custom_tokens)",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.custom_tokens",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.custom_tokens",
          entityAttributeValue: 100,
        },
      ],
      [
        "should extract cost from gen_ai.usage.cost",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.cost",
          otelAttributeValue: {
            doubleValue: 0.000151,
          },
          entityAttributeKey: "costDetails.total",
          entityAttributeValue: 0.000151,
        },
      ],
      [
        "should not treat usage.cost as usage",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.usage.cost",
          otelAttributeValue: {
            doubleValue: 0.000151,
          },
          entityAttributeKey: "usageDetails.cost",
          entityAttributeValue: undefined,
        },
      ],
      [
        "should map gen_ai.prompt.0.content to input[0].content",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.prompt.0.content",
          otelAttributeValue: {
            stringValue: "What is LLM Observability?",
          },
          entityAttributeKey: "input",
          entityAttributeValue: [{ content: "What is LLM Observability?" }],
        },
      ],
      [
        "should map gen_ai.completion.0.content to output[0].content",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.completion.0.content",
          otelAttributeValue: {
            stringValue: "Observing LLMs",
          },
          entityAttributeKey: "output",
          entityAttributeValue: [{ content: "Observing LLMs" }],
        },
      ],
      [
        "should map gen_ai.completion.content to output.content",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.completion.content",
          otelAttributeValue: {
            stringValue: "Observing LLMs",
          },
          entityAttributeKey: "output",
          entityAttributeValue: { content: "Observing LLMs" },
        },
      ],
      [
        "should map gen_ai.completion to output",
        {
          entity: "observation",
          otelAttributeKey: "gen_ai.completion",
          otelAttributeValue: {
            stringValue: "Observing LLMs",
          },
          entityAttributeKey: "output",
          entityAttributeValue: "Observing LLMs",
        },
      ],
      [
        "should map mlflow.spanInputs to input",
        {
          entity: "observation",
          otelAttributeKey: "mlflow.spanInputs",
          otelAttributeValue: {
            stringValue: JSON.stringify({
              question: "What is LLM Observability?",
            }),
          },
          entityAttributeKey: "input",
          entityAttributeValue: JSON.stringify({
            question: "What is LLM Observability?",
          }),
        },
      ],
      [
        "should map model to model",
        {
          entity: "observation",
          otelAttributeKey: "model",
          otelAttributeValue: {
            stringValue: "gpt-4o-mini",
          },
          entityAttributeKey: "model",
          entityAttributeValue: "gpt-4o-mini",
        },
      ],
      [
        "#6084: should map input to input for pydantic",
        {
          entity: "observation",
          otelAttributeKey: "input",
          otelAttributeValue: {
            stringValue: JSON.stringify({
              task: "Play some chess",
              stream: false,
            }),
          },
          entityAttributeKey: "input",
          entityAttributeValue: JSON.stringify({
            task: "Play some chess",
            stream: false,
          }),
        },
      ],
      [
        "#6084: should map model_config to modelParameters",
        {
          entity: "observation",
          otelAttributeKey: "model_config",
          otelAttributeValue: {
            stringValue: '{"max_tokens": 4096}',
          },
          entityAttributeKey: "modelParameters.max_tokens",
          entityAttributeValue: 4096,
        },
      ],
      [
        "#5412: should map input.value to input for smolagents",
        {
          entity: "observation",
          otelAttributeKey: "input.value",
          otelAttributeValue: {
            stringValue: JSON.stringify({
              task: "Play some chess",
              stream: false,
            }),
          },
          entityAttributeKey: "input",
          entityAttributeValue: JSON.stringify({
            task: "Play some chess",
            stream: false,
          }),
        },
      ],
      [
        "#5412: should map llm.token_count.completion to provided_usage_details.output",
        {
          entity: "observation",
          otelAttributeKey: "llm.token_count.completion",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.output",
          entityAttributeValue: 100,
        },
      ],
      [
        "#5412: should map llm.token_count.total to provided_usage_details.total",
        {
          entity: "observation",
          otelAttributeKey: "llm.token_count.total",
          otelAttributeValue: {
            intValue: { low: 100, high: 0, unsigned: false },
          },
          entityAttributeKey: "usageDetails.total",
          entityAttributeValue: 100,
        },
      ],
      [
        "#5412: should map llm.invocation_parameters to modelParameters",
        {
          entity: "observation",
          otelAttributeKey: "llm.invocation_parameters",
          otelAttributeValue: {
            stringValue: '{"max_tokens": 4096}',
          },
          entityAttributeKey: "modelParameters.max_tokens",
          entityAttributeValue: 4096,
        },
      ],
      [
        "#5457: should map traceloop.entity.input to input",
        {
          entity: "trace",
          otelAttributeKey: "traceloop.entity.input",
          otelAttributeValue: {
            stringValue: '{"foo": "bar"}',
          },
          entityAttributeKey: "input",
          entityAttributeValue: '{"foo": "bar"}',
        },
      ],
      [
        "should map langfuse.metadata string to top-level metadata for trace",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.metadata",
          otelAttributeValue: {
            stringValue: '{"customer_id": "123", "experiment": "test-run-1"}',
          },
          entityAttributeKey: "metadata.customer_id",
          entityAttributeValue: "123",
        },
      ],
      [
        "should map langfuse.metadata string to top-level metadata for observation",
        {
          entity: "observation",
          otelAttributeKey: "langfuse.metadata",
          otelAttributeValue: {
            stringValue: '{"customer_id": "123", "experiment": "test-run-1"}',
          },
          entityAttributeKey: "metadata.customer_id",
          entityAttributeValue: "123",
        },
      ],
      [
        "should extract metadata from langfuse.metadata.* keys for trace",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.metadata.user_type",
          otelAttributeValue: {
            stringValue: "premium",
          },
          entityAttributeKey: "metadata.user_type",
          entityAttributeValue: "premium",
        },
      ],
      [
        "should extract metadata from langfuse.metadata.* keys for observation",
        {
          entity: "observation",
          otelAttributeKey: "langfuse.metadata.user_type",
          otelAttributeValue: {
            stringValue: "premium",
          },
          entityAttributeKey: "metadata.user_type",
          entityAttributeValue: "premium",
        },
      ],
      [
        "should extract tags from single string from langfuse.tags to trace",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.tags",
          otelAttributeValue: {
            stringValue: "2",
          },
          entityAttributeKey: "tags",
          entityAttributeValue: ["2"],
        },
      ],
      [
        "should extract array input on trace event attributes",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.tags",
          otelAttributeValue: {
            arrayValue: {
              values: [
                {
                  stringValue: "2",
                },
              ],
            },
          },
          entityAttributeKey: "tags",
          entityAttributeValue: ["2"],
        },
      ],
      [
        "should extract array input tags to trace",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.tags",
          otelAttributeValue: {
            stringValue: '["2"]',
          },
          entityAttributeKey: "tags",
          entityAttributeValue: ["2"],
        },
      ],
      [
        "should extract array csv input tags to trace",
        {
          entity: "trace",
          otelAttributeKey: "langfuse.tags",
          otelAttributeValue: {
            stringValue: "2,3,4",
          },
          entityAttributeKey: "tags",
          entityAttributeValue: ["2", "3", "4"],
        },
      ],
    ])(
      "Attributes: %s",
      (
        _name: string,
        spec: {
          entity: string;
          otelAttributeKey: string;
          otelAttributeValue: any;
          entityAttributeKey: string;
          entityAttributeValue: any;
        },
      ) => {
        // Setup
        const resourceSpan = {
          scopeSpans: [
            {
              spans: [
                {
                  ...defaultSpanProps,
                  attributes: [
                    {
                      key: spec.otelAttributeKey,
                      value: spec.otelAttributeValue,
                    },
                  ],
                },
              ],
            },
          ],
        };

        // When
        const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

        // Then
        const entity: { body: Record<string, any> } =
          spec.entity === "trace" ? langfuseEvents[0] : langfuseEvents[1];
        expect(
          spec.entityAttributeKey // This logic allows to follow a path in the object, e.g. foo.bar.baz.
            .split(".")
            .reduce((acc: any, key: string) => acc && acc[key], entity.body),
        ).toEqual(spec.entityAttributeValue);
      },
    );

    it.each([
      [
        "should extract version on trace from resource attribute service.version",
        {
          entity: "trace",
          otelResourceAttributeKey: "service.version",
          otelResourceAttributeValue: { stringValue: "1.0.5" },
          entityAttributeKey: "version",
          entityAttributeValue: "1.0.5",
        },
      ],
      [
        "should extract environment on trace for langfuse.environment",
        {
          entity: "trace",
          otelResourceAttributeKey: "langfuse.environment",
          otelResourceAttributeValue: { stringValue: "test" },
          entityAttributeKey: "environment",
          entityAttributeValue: "test",
        },
      ],
      [
        "should extract environment on observation for deployment.environment.name",
        {
          entity: "observation",
          otelResourceAttributeKey: "deployment.environment.name",
          otelResourceAttributeValue: { stringValue: "test" },
          entityAttributeKey: "environment",
          entityAttributeValue: "test",
        },
      ],
      [
        "should fallback to default on observation if no environment present",
        {
          entity: "observation",
          otelResourceAttributeKey: "unused.key",
          otelResourceAttributeValue: { stringValue: "" },
          entityAttributeKey: "environment",
          entityAttributeValue: "default",
        },
      ],
      [
        "should extract metadata from resource attributes",
        {
          entity: "observation",
          otelResourceAttributeKey: "langfuse.metadata",
          otelResourceAttributeValue: {
            stringValue: '{"resource_id": "xyz", "region": "us-west-2"}',
          },
          entityAttributeKey: "metadata.resource_id",
          entityAttributeValue: "xyz",
        },
      ],
      [
        "should extract metadata from langfuse.metadata.* resource attributes",
        {
          entity: "observation",
          otelResourceAttributeKey: "langfuse.metadata.server_name",
          otelResourceAttributeValue: {
            stringValue: "web-server-01",
          },
          entityAttributeKey: "metadata.server_name",
          entityAttributeValue: "web-server-01",
        },
      ],
    ])(
      "ResourceAttributes: %s",
      (
        _name: string,
        spec: {
          entity: string;
          otelResourceAttributeKey: string;
          otelResourceAttributeValue: any;
          entityAttributeKey: string;
          entityAttributeValue: any;
        },
      ) => {
        // Setup
        const resourceSpan = {
          resource: {
            attributes: [
              {
                key: spec.otelResourceAttributeKey,
                value: spec.otelResourceAttributeValue,
              },
            ],
          },
          scopeSpans: [
            {
              spans: [defaultSpanProps],
            },
          ],
        };

        // When
        const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

        // Then
        const entity: { body: Record<string, any> } =
          spec.entity === "trace" ? langfuseEvents[0] : langfuseEvents[1];
        expect(
          spec.entityAttributeKey // This logic allows to follow a path in the object, e.g. foo.bar.baz.
            .split(".")
            .reduce((acc: any, key: string) => acc && acc[key], entity.body),
        ).toEqual(spec.entityAttributeValue);
      },
    );

    it.each([
      [
        "should extract input on trace from event attributes",
        {
          entity: "trace",
          otelEventName: "gen_ai.content.prompt",
          otelEventAttributeKey: "gen_ai.prompt",
          otelEventAttributeValue: {
            stringValue: "user: What is LLM Observability?",
          },
          entityAttributeKey: "input",
          entityAttributeValue: "user: What is LLM Observability?",
        },
      ],
      [
        "should extract array input on trace event attributes",
        {
          entity: "trace",
          otelEventName: "gen_ai.content.prompt",
          otelEventAttributeKey: "gen_ai.prompt",
          otelEventAttributeValue: {
            arrayValue: {
              values: [
                {
                  stringValue: "Reply with the word 'java'",
                },
              ],
            },
          },
          entityAttributeKey: "input",
          entityAttributeValue: ["Reply with the word 'java'"],
        },
      ],
      [
        "should extract output on observation from event attributes",
        {
          entity: "observation",
          otelEventName: "gen_ai.content.completion",
          otelEventAttributeKey: "gen_ai.completion",
          otelEventAttributeValue: {
            stringValue:
              "assistant: LLM Observability stands for logs, metrics, and traces observability.",
          },
          entityAttributeKey: "output",
          entityAttributeValue:
            "assistant: LLM Observability stands for logs, metrics, and traces observability.",
        },
      ],
      [
        "should extract output on observation from event attributes even if no gen_ai.completion attribute is available",
        {
          entity: "observation",
          otelEventName: "gen_ai.content.completion",
          otelEventAttributeKey: "gen_ai.something_else",
          otelEventAttributeValue: {
            stringValue:
              "assistant: LLM Observability stands for logs, metrics, and traces observability.",
          },
          entityAttributeKey: "output",
          entityAttributeValue: {
            "gen_ai.something_else":
              "assistant: LLM Observability stands for logs, metrics, and traces observability.",
          },
        },
      ],
      // Semantic Kernel 1.55+ new event-based semantic conventions
      [
        "should extract input from gen_ai.system.message event",
        {
          entity: "observation",
          otelEventName: "gen_ai.system.message",
          otelEventAttributeKey: "content",
          otelEventAttributeValue: {
            stringValue: "You are a helpful assistant.",
          },
          entityAttributeKey: "input",
          entityAttributeValue: [
            {
              role: "system",
              content: "You are a helpful assistant.",
            },
          ],
        },
      ],
      [
        "should extract input from gen_ai.user.message event",
        {
          entity: "observation",
          otelEventName: "gen_ai.user.message",
          otelEventAttributeKey: "content",
          otelEventAttributeValue: {
            stringValue: "What is the capital of France?",
          },
          entityAttributeKey: "input",
          entityAttributeValue: [
            {
              role: "user",
              content: "What is the capital of France?",
            },
          ],
        },
      ],
      [
        "should extract input from gen_ai.assistant.message event with tool_calls",
        {
          entity: "observation",
          otelEventName: "gen_ai.assistant.message",
          otelEventAttributeKey: "tool_calls",
          otelEventAttributeValue: {
            stringValue: JSON.stringify([{
              id: "call_123",
              type: "function",
              function: {
                name: "get_weather",
                arguments: '{"location": "Paris"}'
              }
            }]),
          },
          entityAttributeKey: "input",
          entityAttributeValue: [
            {
              role: "assistant",
              tool_calls: JSON.stringify([{
                id: "call_123",
                type: "function",
                function: {
                  name: "get_weather",
                  arguments: '{"location": "Paris"}'
                }
              }]),
            },
          ],
        },
      ],
      [
        "should extract input from gen_ai.tool.message event",
        {
          entity: "observation",
          otelEventName: "gen_ai.tool.message",
          otelEventAttributeKey: "content",
          otelEventAttributeValue: {
            stringValue: "Sunny, 22°C",
          },
          entityAttributeKey: "input",
          entityAttributeValue: [
            {
              role: "tool",
              content: "Sunny, 22°C",
            },
          ],
        },
      ],
      [
        "should extract output from gen_ai.choice event",
        {
          entity: "observation",
          otelEventName: "gen_ai.choice",
          otelEventAttributeKey: "message",
          otelEventAttributeValue: {
            stringValue: JSON.stringify({
              role: "assistant",
              content: "The capital of France is Paris.",
            }),
          },
          entityAttributeKey: "output",
          entityAttributeValue: {
            message: JSON.stringify({
              role: "assistant",
              content: "The capital of France is Paris.",
            }),
          },
        },
      ],
      [
        "should extract output from gen_ai.choice event with finish_reason",
        {
          entity: "observation",
          otelEventName: "gen_ai.choice",
          otelEventAttributeKey: "finish_reason",
          otelEventAttributeValue: {
            stringValue: "stop",
          },
          entityAttributeKey: "output",
          entityAttributeValue: {
            finish_reason: "stop",
          },
        },
      ],
      [
        "should extract input from gen_ai.tool.message event with id attribute",
        {
          entity: "observation",
          otelEventName: "gen_ai.tool.message",
          otelEventAttributeKey: "id",
          otelEventAttributeValue: {
            stringValue: "call_456",
          },
          entityAttributeKey: "input",
          entityAttributeValue: [
            {
              role: "tool",
              id: "call_456",
            },
          ],
        },
      ],
      [
        "should extract output from gen_ai.choice event with index attribute",
        {
          entity: "observation",
          otelEventName: "gen_ai.choice",
          otelEventAttributeKey: "index",
          otelEventAttributeValue: {
            intValue: { low: 0, high: 0, unsigned: false },
          },
          entityAttributeKey: "output",
          entityAttributeValue: {
            index: 0,
          },
        },
      ],
      [
        "should extract input from gen_ai.assistant.message event with content",
        {
          entity: "observation",
          otelEventName: "gen_ai.assistant.message",
          otelEventAttributeKey: "content",
          otelEventAttributeValue: {
            stringValue: "I'll help you with that.",
          },
          entityAttributeKey: "input",
          entityAttributeValue: [
            {
              role: "assistant",
              content: "I'll help you with that.",
            },
          ],
        },
      ],
    ])(
      "Events: %s",
      (
        _name: string,
        spec: {
          entity: string;
          otelEventName: string;
          otelEventAttributeKey: string;
          otelEventAttributeValue: any;
          entityAttributeKey: string;
          entityAttributeValue: any;
        },
      ) => {
        // Setup
        const resourceSpan = {
          resource: {},
          scopeSpans: [
            {
              spans: [
                {
                  ...defaultSpanProps,
                  events: [
                    {
                      timeUnixNano: {
                        low: 1327691067,
                        high: 404677085,
                        unsigned: true,
                      },
                      name: spec.otelEventName,
                      attributes: [
                        {
                          key: spec.otelEventAttributeKey,
                          value: spec.otelEventAttributeValue,
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          ],
        };

        // When
        const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);

        // Then
        const entity: { body: Record<string, any> } =
          spec.entity === "trace" ? langfuseEvents[0] : langfuseEvents[1];
        expect(entity.body[spec.entityAttributeKey]).toEqual(
          spec.entityAttributeValue,
        );
      },
    );
  });

  describe("Timestamp Conversion", () => {
    it("should correctly convert OpenTelemetry timestamps to ISO strings", () => {
      // Test case with positive low value
      const positiveTimestamp = {
        low: 1095848032,
        high: 406260507,
        unsigned: true,
      };

      // Test case with negative low value
      const negativeTimestamp = {
        low: -1431863980,
        high: 406260507,
        unsigned: true,
      };

      // Expected ISO strings based on the provided mapping
      const expectedStartTime = "2025-04-17T07:39:52.317Z";
      const expectedEndTime = "2025-04-17T07:39:54.084Z";

      // Convert timestamps to ISO strings
      const actualStartTime = convertNanoTimestampToISO(positiveTimestamp);
      const actualEndTime = convertNanoTimestampToISO(negativeTimestamp);

      // Verify conversions match expected values
      expect(actualStartTime).toBe(expectedStartTime);
      expect(actualEndTime).toBe(expectedEndTime);
    });

    it("should handle various timestamp formats correctly", () => {
      // Test with string timestamp (nanoseconds)
      const stringTimestamp = "1744317592317227000"; // Same as positiveTimestamp above
      const expectedStringResult = "2025-04-10T20:39:52.317Z";
      expect(convertNanoTimestampToISO(stringTimestamp)).toBe(
        expectedStringResult,
      );

      // Test with zero timestamp
      const zeroTimestamp = {
        low: 0,
        high: 0,
        unsigned: true,
      };
      expect(convertNanoTimestampToISO(zeroTimestamp)).toBe(
        "1970-01-01T00:00:00.000Z",
      );
    });
  });
});
